// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2020, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * Support usb phy for lombo platforms
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 */
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/of_device.h>
#include <linux/phy/phy.h>
#include <linux/clk-provider.h>
#include <linux/of_device.h>
#include <linux/regmap.h>
#include <linux/mfd/syscon.h>

/* n9v1 usb phy prcm reg address */
#define N9V1_USB2DRD0_PHY_PRCM_REG_ADDR		0x0400ace0
#define N9V1_USB2DRD1_PHY_PRCM_REG_ADDR		0x0400ace8
#define N9V1_USB2HOST0_PHY_PRCM_REG_ADDR	0x0400ace0
#define N9V1_USB3DRD0_PHY_PRCM_REG_ADDR		0x0400ace4

/* n9v1 usb phy reg address */
#define N9V1_USB2DRD0_PHY_REG_BASE_ADDR		(0x02200000 + 0x80000)
#define N9V1_USB2DRD1_PHY_REG_BASE_ADDR		(0x01E00000 + 0x100000)
#define N9V1_USB2HOST0_PHY_REG_BASE_ADDR	(0x02300000 + 0x80000)
#define N9V1_USB3DRD0_PHY_REG_BASE_ADDR		(0x01C00000 + 0x100000)

/*
 * @phy: point to the phy
 * @phy_prcm: the ioremap of the phy prcm reg address
 * @phy_base: the ioremap of the phy reg base address
 * @phy_initialized: the phy initialized flag
 * @mode: the phy mode(host/peripheral/drd)
 * @vbus_gpio: the gpio of vbus control
 * @name: the phy's name
 * @data: the phy private data
 */
struct lombo_usb_phy {
	struct phy *phy;

	void __iomem *prcm;
	void __iomem *phy_base;

	bool phy_initialized;
	enum phy_mode mode;

	int vbus_gpio;

	char *name;
	const struct lb_usb_phy_data *data;
};

struct lb_usb_phy_data {
	u32 prcm_reg_addr;
	u32 phy_reg;
	char *phy_name;
};

struct lb_usb_phy_data n9v1_usb2drd0_phy_data = {
	.prcm_reg_addr	= N9V1_USB2DRD0_PHY_PRCM_REG_ADDR,
	.phy_reg	= N9V1_USB2DRD0_PHY_REG_BASE_ADDR,
	.phy_name	= "n9v1-usb2drd0-phy",
};

struct lb_usb_phy_data n9v1_usb2drd1_phy_data = {
	.prcm_reg_addr	= N9V1_USB2DRD1_PHY_PRCM_REG_ADDR,
	.phy_reg	= N9V1_USB2DRD1_PHY_REG_BASE_ADDR,
	.phy_name	= "n9v1-usb2drd1-phy",
};

struct lb_usb_phy_data n9v1_usb2host0_phy_data = {
	.prcm_reg_addr	= N9V1_USB2HOST0_PHY_PRCM_REG_ADDR,
	.phy_reg	= N9V1_USB2HOST0_PHY_REG_BASE_ADDR,
	.phy_name	= "n9v1-usb2host0-phy",
};

struct lb_usb_phy_data n9v1_usb3drd0_phy_data = {
	.prcm_reg_addr	= N9V1_USB3DRD0_PHY_PRCM_REG_ADDR,
	.phy_reg	= N9V1_USB3DRD0_PHY_REG_BASE_ADDR,
	.phy_name	= "n9v1-usb3drd0-phy",
};

static const struct of_device_id lb_usb_phy_match[] = {
	{ .compatible = "lombo,n9v1-usb2drd0-phy",
	.data = &n9v1_usb2drd0_phy_data },

	{ .compatible = "lombo,n9v1-usb2drd1-phy",
	.data = &n9v1_usb2drd1_phy_data},

	{ .compatible = "lombo,n9v1-usb2host0-phy",
	.data = &n9v1_usb2host0_phy_data},

	{ .compatible = "lombo,n9v1-usb3drd0-phy",
	.data = &n9v1_usb3drd0_phy_data},

	{ .compatible = "lombo,n9-usb-phy" },

	{},
};
MODULE_DEVICE_TABLE(of, lb_usb_phy_match);

static void lb_usb_phy_vbus_enable(struct lombo_usb_phy *lb_phy)
{
	int ret;
	int gpio_num;

	gpio_num = lb_phy->vbus_gpio;
	if (gpio_num >= 0) {
		ret = gpio_direction_output(gpio_num, 1);
		if (ret)
			dev_warn(&lb_phy->phy->dev,
				"vbus gpio output hight fail:%d!\n", ret);
	} else
		dev_err(&lb_phy->phy->dev, "%s err!\n", __func__);
}

static void lb_usb_phy_vbus_disable(struct lombo_usb_phy *lb_phy)
{
	int ret;
	int gpio_num;

	gpio_num = lb_phy->vbus_gpio;
	if (gpio_num >= 0) {
		ret = gpio_direction_output(gpio_num, 0);
		if (ret) {
			dev_warn(&lb_phy->phy->dev,
				"vbus gpio output low fail:%d!\n", ret);
		}
	} else
		dev_err(&lb_phy->phy->dev, "%s err!\n", __func__);
}

enum phy_reset_ctrl {
	ASSERTED_RESET = 0,
	DEASSERTED_NOTRESET,
};

static void lb_phy_reset_ctrl(struct lombo_usb_phy *lb_phy,
				enum phy_reset_ctrl ctrl)
{
	int val;
	void __iomem *base = lb_phy->prcm;

	val = readl(base);
	if (ctrl == ASSERTED_RESET) {
		if (!strcmp(lb_phy->name, "n9v1-usb2drd0-phy"))
			val &= ~BIT(0);
		else if (!strcmp(lb_phy->name, "n9v1-usb2host0-phy"))
			val &= ~BIT(1);
	} else if (ctrl == DEASSERTED_NOTRESET) {
		if (!strcmp(lb_phy->name, "n9v1-usb2drd0-phy"))
			val |= BIT(0);
		else if (!strcmp(lb_phy->name, "n9v1-usb2host0-phy"))
			val |= BIT(1);
	}
	writel(val, base);

	udelay(3);
}

int lb_usb_phy_reset(struct phy *phy)
{
	struct lombo_usb_phy *lb_phy = phy_get_drvdata(phy);

	lb_phy_reset_ctrl(lb_phy, ASSERTED_RESET);
	lb_phy_reset_ctrl(lb_phy, DEASSERTED_NOTRESET);

	return 0;
}

static int lb_usb_phy_init(struct phy *phy)
{
	struct lombo_usb_phy *lb_phy = phy_get_drvdata(phy);

	dev_info(&phy->dev, "%s\n", __func__);

	lb_phy->phy_initialized = true;

	return 0;
}

static int lb_usb_phy_exit(struct phy *phy)
{
	struct lombo_usb_phy *lb_phy = phy_get_drvdata(phy);

	dev_info(&phy->dev, "%s\n", __func__);

	lb_phy->phy_initialized = false;

	return 0;
}

int lb_usb_phy_set_mode(struct phy *phy, enum phy_mode mode)
{
	int ret = 0;
	struct lombo_usb_phy *lb_phy = phy_get_drvdata(phy);

	dev_info(&phy->dev, "%s\n", __func__);

	switch (mode) {
	case PHY_MODE_USB_HOST:
		lb_usb_phy_vbus_enable(lb_phy);
		break;
	case PHY_MODE_USB_DEVICE:
		lb_usb_phy_vbus_disable(lb_phy);
		break;
	case PHY_MODE_USB_OTG:
		lb_usb_phy_vbus_enable(lb_phy);
		break;
	default:
		dev_info(&phy->dev, "%s EINVAL", __func__);
		ret = -EINVAL;
		break;
	}

	return ret;
}

static const struct phy_ops lb_usb_phy_ops = {
	.init		= lb_usb_phy_init,
	.exit		= lb_usb_phy_exit,
	.set_mode	= lb_usb_phy_set_mode,
	.reset		= lb_usb_phy_reset,
	.owner		= THIS_MODULE,
};

static int lb_usb_phy_probe(struct platform_device *pdev)
{
	int ret;
	int gpio_num;

	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node;
	struct lombo_usb_phy *lb_phy;
	struct phy *generic_phy;
	struct phy_provider *phy_provider;
	const struct of_device_id *of_id = of_match_device(lb_usb_phy_match,
								&pdev->dev);

	lb_phy = devm_kzalloc(dev, sizeof(struct lombo_usb_phy), GFP_KERNEL);
	if (!lb_phy)
		return -ENOMEM;

	of_id = of_match_device(lb_usb_phy_match, &pdev->dev);

	generic_phy = devm_phy_create(dev, NULL, &lb_usb_phy_ops);
	if (IS_ERR(generic_phy)) {
		ret = PTR_ERR(generic_phy);
		dev_err(dev, "failed to create phy, %d\n", ret);
		return ret;
	}
	lb_phy->phy = generic_phy;
	lb_phy->data = of_id->data;
	dev_set_drvdata(dev, lb_phy);
	phy_set_drvdata(generic_phy, lb_phy);

	lb_phy->name = lb_phy->data->phy_name;
	lb_phy->prcm = ioremap(lb_phy->data->prcm_reg_addr, 4);
	lb_phy->phy_base = ioremap(lb_phy->data->phy_reg, 4);

	gpio_num = of_get_named_gpio(np, "gpio-vbus", 0);
	if (gpio_num >= 0) {
		dev_info(dev, "of_get_named_gpio, gpio_num: %d.\n", gpio_num);
		lb_phy->vbus_gpio = gpio_num;
		ret = devm_gpio_request(&pdev->dev, gpio_num, lb_phy->name);
		if (ret < 0)
			dev_warn(dev, "usb vbus gpio request err: %d.\n", ret);
	} else
		dev_warn(dev, "of_get_named_gpio err.\n");

	phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
	if (IS_ERR(phy_provider))
		dev_err(dev, "register lb usb phy failed\n");
	else
		dev_info(dev, "register lb usb phy ok\n");

	lb_usb_phy_reset(lb_phy->phy);

	dev_info(dev, "%s init done!\n", lb_phy->name);

	return PTR_ERR_OR_ZERO(phy_provider);
}

static struct platform_driver lb_usb_phy_driver = {
	.probe		= lb_usb_phy_probe,
	.driver		= {
		.name	= "lombo-usb-phy",
		.of_match_table = lb_usb_phy_match,
	},
};

module_platform_driver(lb_usb_phy_driver);

MODULE_ALIAS("platform:lb-usb-phy");
MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_DESCRIPTION("Driver for LomboTech usb phy");
MODULE_LICENSE("GPL");
